package org.sgx.dao;

import java.sql.ResultSet;
import java.util.List;

import org.sgx.dao.model.Model;
import org.sgx.dao.util.BeanUtil;
import org.sgx.dao.util.SQLUtil;
import org.sgx.dao.util.Util;

/**
 * with this helper class relationship between DAO managed java classes can be defined. 
 * formally this represent a bynary many-to-many relationship. A relationship is then 
 * defined by 2 dao objects (D1 is the source and D2 is the end of the relationship), and
 * a relationship name. Each relationship will define a sql table with the name:
 * 
 * TABLE_PREFIX+dao1.getTableName()+"_"+dao2.getTableName()+"_"+relName;
 * 
 * <pre>
 * Tree tree1 = new Tree(...);
 * Leaf l1 = new Leaf(...);
 * 
 * Dao<Tree>treeDao = new BeanDao<Tree>(...); 
 * Dao<Leaf>leafDao = new BeanDao<Leaf>(...); 
 * 
 * treeDao.add(tree1);
 * leafDao.add(l1);
 * 
 * 
 * Relationship treeLeavesRel = new Relationship("leaves", treeDao, leafDao);
 * treeLeavesRel.add(tree1, l1);
 * ...
 * if(treeLeavesRel.related(tree1, l1)){...}
 * ...
 * treeLeavesRel.remove(tree1, l1);
 * ... 
 * List<Leaf>tree1Leaves = treeLeavesRel.getEnds(tree1);
 * 
 * 
 *   //please note that is it possible to use in the other direction (many to many):
 * Tree tree2 = new Tree(...);
 * treeDao.add(tree2);
 * treeLeavesRel.add(tree2, l1); //asociate the same leaf to another tree.
 * assertTrue(treeLeavesRel.getSources(l1).size()==2);
 * 
 * </pre>
 * 
 * <br/>Relationships also support Locale operations, but relatioships won't not hold any information about object locales. 
 * @author sg
 *
 * @param <T1>
 * @param <T2>
 */
public class Relationship<T1 extends Model, T2 extends Model> {
	
//private static final String TABLE_PREFIX = "_rel_";
private static final String FIELD_DAO1 = "dao1Id", FIELD_DAO2="dao2Id";

//	public static final String TYPE_ONE_TO_MANY = "one-to-many";
	private Dao<T1> dao1;
	private Dao<T2> dao2;
	private String relName;
	private String relTableName;
	private SQLUtil sql;

	/**
	 * of course, the two daos must share the same connection giver...
	 * @param relName
	 * @param dao1
	 * @param dao2
	 * @throws DaoException 
	 */
	public Relationship(String relName, Dao<T1> dao1, Dao<T2> dao2, String relTableName_) throws DaoException {
		this.relName=relName;
		this.dao1=dao1;
		this.dao2=dao2;	
		
		relTableName_=relTableName_==null ? getDefaultRelTableName() : relTableName_;
		this.relTableName=relTableName_;
		if(!this.relTableName.startsWith(SQLUtil.TABLE_PREFIX()))
			this.relTableName=SQLUtil.TABLE_PREFIX()+this.relTableName;
		
		sql = new SQLUtil(dao1.getConnectionGiver());
		if(!sql.tableExists(relTableName)) {
			sql.tableCreate(relTableName, Util.<String>toOrderedMap(
				FIELD_DAO1, SQLUtil.TYPE_LONG,
				FIELD_DAO2, SQLUtil.TYPE_LONG
			));
		}
	}
	public Relationship(String relName, Dao<T1> dao1, Dao<T2> dao2) throws DaoException {
		this(relName, dao1, dao2, null);
	}
	private String getDefaultRelTableName() {
		return SQLUtil.TABLE_PREFIX()+dao1.getTableName()+"_"+dao2.getTableName()+"_"+relName;
	}

	private static class LongBuilder implements SQLUtil.SQLObjectBuilder<Long> {
		private String colName;
		public LongBuilder(String colName) {
			this.colName = colName;
		}
		public Long buildObjectFrom(ResultSet rs) throws Exception {
			return rs.getLong(colName);
		}		
	}
	
	/**
	 * get all T2 class objects related to source t1 by this relationship.
	 * @param t1
	 * @return
	 * @throws DaoException
	 * TODO: make this more efficient doing more presice queries 
	 */
	public List<T2> getEnds(T1 t1) throws DaoException {
		return getLocalizedEnds(t1, null);
	}
	public List<T2> getLocalizedEnds(T1 t1, String locale) throws DaoException {
		List<Long> t2Ids = sql.query(relTableName, 
			Util.<Object>toOrderedMap(FIELD_DAO1, getModelId(t1)), 
			"AND", new LongBuilder(FIELD_DAO2));
		if(locale==null)
			return dao2.get(t2Ids);
		else
			return dao2.getLocales(t2Ids, locale);
	}
	/**
	 * get the single object related to t2 destination object in this relationship.
	 * @param t2
	 * @return
	 * @throws DaoException
	 */
	public List<T1> getSources(T2 t2) throws DaoException {
		return getLocalizedSources(t2, null);
	}
	public List<T1> getLocalizedSources(T2 t2, String locale) throws DaoException {
		List<Long> t1Ids = sql.query(relTableName, 
				Util.<Object>toOrderedMap(FIELD_DAO2, getModelId(t2)), 
				"AND", new LongBuilder(FIELD_DAO1));		
		if(locale==null)
			return dao1.get(t1Ids);
		else
			return dao1.getLocales(t1Ids, locale);
	}
	
	public boolean related(T1 t1, T2 t2) throws DaoException {
		return sql.query(relTableName, Util.<Object>toOrderedMap(
			FIELD_DAO1, getModelId(t1),
			FIELD_DAO2, getModelId(t2)
		), 	"AND", null).size()>0;
	}
	
	/**
	 * add t2 to t1 in the relationship defined by this. if t2 is aready related, this do not nothing.
	 * @param t1
	 * @param t2
	 * @throws DaoException 
	 */
	public int add(T1 t1, T2 t2) throws DaoException {
		if(!related(t1, t2)){//get(t1).size()==0) {
			sql.insert(Util.<Object>toOrderedMap(
				FIELD_DAO1, getModelId(t1), 
				FIELD_DAO2, getModelId(t2)),
			relTableName);
			return 1;
		} 
		else
			return 0;
	}
	
	protected long getModelId(Model m) throws DaoException {
		try {
			return (Long) BeanUtil.getProperty(m, m.getFieldIdName());
		} catch (Exception e) {
			throw new DaoException(e);
		}
	}
	public void removeSources(List<T1> sources) throws DaoException {
		for(T1 t1 : sources) {
			removeSource(t1);
		}
	}
	public void removeSource(T1 t1) throws DaoException {
		removeSource(getModelId(t1));
	}
	public void removeSource(Long id) throws DaoException {
		sql.delete(relTableName, Util.<Object>toOrderedMap(
			FIELD_DAO1, id
		));
	}
	public void remove(T1 t1, T2 t2) throws DaoException {
		sql.delete(relTableName, Util.<Object>toOrderedMap(
			FIELD_DAO1, getModelId(t1),
			FIELD_DAO2, getModelId(t2)
		), "and");
	}
	public void removeEnds(List<T2> ends) throws DaoException {
		for(T2 t2 : ends) {
			removeEnd(t2);
		}
	}
	public void removeEnd(T2 t2) throws DaoException {
		removeEnd(getModelId(t2));
	}
	public void removeEnd(long id) throws DaoException {
		sql.delete(relTableName, Util.<Object>toOrderedMap(
			FIELD_DAO2, id
		));
	}
}
